AndroidNativeEmu 调用 JNI 函数和模拟 Java 函数交互
AndroidNativeEmu 调用 JNI 函数和模拟 Java 函数交互
AndroidNativeEmu 是一个基于 Unicorn 引擎的 Android Native 函数模拟执行框架,用 Python 编写。它提供了比原生 Unicorn 更高级的抽象,能够模拟 JNI 环境、加载 SO 文件、调用导出函数等。与 Unidbg(Java 实现)相比,AndroidNativeEmu 的门槛更低,适合 Python 开发者快速上手。本文将详细介绍 AndroidNativeEmu 的使用方法。
AndroidNativeEmu 框架介绍
AndroidNativeEmu 的核心设计目标是让用户能够以尽可能简单的方式模拟执行 Android SO 文件中的 Native 函数。它的主要特性包括:
- Python 实现:纯 Python 编写,学习成本低,适合快速原型开发
- 基于 Unicorn:底层使用 Unicorn 进行 CPU 指令模拟
- JNI 环境模拟:内置了 JNI 函数表的基础模拟
- SO 加载:支持加载 ELF 格式的 SO 文件
- Java 对象模拟:可以模拟 Java 层的对象和方法调用
- 可扩展:支持自定义 JNI 函数实现和 Java 方法模拟
与 Unidbg 的对比
| 特性 | AndroidNativeEmu | Unidbg |
|---|---|---|
| 语言 | Python | Java |
| 底层引擎 | Unicorn | Unicorn |
| JNI 模拟 | 基础实现 | 完善 |
| 系统库模拟 | 部分 | 完善 |
| 调试支持 | 基础 | 丰富 |
| 学习曲线 | 较低 | 中等 |
| 维护状态 | 社区维护 | 活跃维护 |
| 适用场景 | 简单算法调用、快速验证 | 复杂 SO 分析、生产环境 |
安装
git clone https://github.com/AeonLucid/AndroidNativeEmu.git
cd AndroidNativeEmu
pip install -r requirements.txt
模拟 JNI 环境的搭建
创建模拟器实例
from androidemu.emulator import Emulator
from androidemu.utils import memory_helpers
# 创建模拟器
emulator = Emulator()
# 加载 SO 文件
lib_module = emulator.load_library(
"lib/armeabi-v7a/libnative.so"
)
JNIEnv 函数表模拟
AndroidNativeEmu 内部维护了一个 JNI 函数表(JNINativeInterface),每个 JNI 函数对应一个 Python 实现。当 Native 代码调用 JNI 函数时(如 GetStringUTFChars),AndroidNativeEmu 会调用对应的 Python 处理函数。
JNI 函数表的初始化过程大致如下:
# AndroidNativeEmu 内部的 JNI 函数表结构
# 每个索引对应一个 JNI 函数
JNI_FUNCTIONS = {
4: "GetVersion",
6: "DefineClass",
7: "FindClass",
10: "GetSuperclass",
11: "IsAssignableFrom",
# ... 更多 JNI 函数
154: "GetStringUTFChars",
155: "ReleaseStringUTFChars",
164: "GetStringUTFLength",
# ... FindClass, RegisterNatives 等
215: "RegisterNatives",
# ...
}
模拟 Java 函数
当 SO 文件中的 Native 函数回调 Java 层方法时,AndroidNativeEmu 需要模拟这些 Java 方法的行为。
处理 registerNativeMethods
很多 SO 文件在 JNI_OnLoad 中通过 RegisterNatives 动态注册 JNI 方法。AndroidNativeEmu 会自动处理这个注册过程:
# AndroidNativeEmu 加载 SO 时会自动调用 JNI_OnLoad
# 如果 SO 中使用了 RegisterNatives,框架会记录注册的方法映射
lib_module = emulator.load_library("lib/armeabi-v7a/libsign.so")
调用动态注册的函数时,可以直接使用方法名:
# 调用通过 RegisterNatives 注册的函数
result = emulator.call_symbol(
"native_sign_func", # 通过 RegisterNatives 注册的函数符号
arg1, arg2
)
模拟 System.currentTimeMillis
许多 APP 的签名算法会获取当前时间戳作为参数的一部分。在模拟执行时,需要模拟这个 Java 方法:
import time
def hook_system_currenttimemillis(emu):
"""模拟 System.currentTimeMillis()"""
# 返回当前时间戳(毫秒)
current_time = int(time.time() * 1000)
return current_time
# 注册 Java 方法模拟
emulator.java_method_manager.add_method(
"java/lang/System",
"currentTimeMillis",
"()J",
hook_system_currenttimemillis
)
如果需要固定时间戳(用于复现结果):
FIXED_TIMESTAMP = 1700000000000 # 固定时间戳
def hook_system_currenttimemillis_fixed(emu):
return FIXED_TIMESTAMP
emulator.java_method_manager.add_method(
"java/lang/System",
"currentTimeMillis",
"()J",
hook_system_currenttimemillis_fixed
)
模拟 getString 等常用 JNI 函数
GetStringUTFChars 是 JNI 中最常用的函数之一,Native 代码通过它获取 Java 字符串的内容:
# AndroidNativeEmu 默认已经实现了基础的 GetStringUTFChars
# 它会返回 Java 字符串对象的实际内容
# 如果需要自定义行为,可以覆盖默认实现
def custom_get_string_utf_chars(emu, jstring):
"""自定义 GetStringUTFChars 实现"""
# jstring 是一个指向 Java 字符串对象的指针
# 读取其内容
string_ptr = memory_helpers.read_utf8(emu, jstring)
return string_ptr
# 替换默认实现
emulator.jnienv.set_string_utf_chars_handler(custom_get_string_utf_chars)
模拟 GetMethodID 和 CallObjectMethod
当 Native 代码需要回调 Java 方法时,会使用 GetMethodID 查找方法,然后通过 CallObjectMethod 调用:
# 模拟 GetMethodID
def hook_get_method_id(emu, env, clazz, name, sig):
"""模拟 FindClass 和 GetMethodID"""
print(f"[JNI] GetMethodID: {name} {sig}")
# 返回一个假的 method ID
return 0x1234
# 模拟 CallObjectMethod
def hook_call_object_method(emu, env, obj, method_id, *args):
"""模拟 Java 方法调用"""
print(f"[JNI] CallObjectMethod: method_id={method_id:#x}")
# 根据不同的 method_id 返回不同的结果
if method_id == 0x1234:
# 返回一个模拟的 Java 字符串对象
return emulator.create_java_string("simulated_result")
return 0
处理 SO 中的加密调用
模拟 Base64 编解码
许多签名算法在 Native 层使用 Base64 编码。需要模拟 android.util.Base64 类:
import base64
def hook_base64_encode(emu, input_bytes, flags):
"""模拟 Base64.encodeToString"""
encoded = base64.b64encode(input_bytes).decode('utf-8')
return emu.create_java_string(encoded)
def hook_base64_decode(emu, input_string, flags):
"""模拟 Base64.decode"""
decoded = base64.b64decode(input_string)
return emulator.create_java_byte_array(decoded)
模拟 MessageDigest(MD5/SHA)
import hashlib
def hook_md5_digest(emu, input_bytes):
"""模拟 MessageDigest.digest()"""
md5 = hashlib.md5()
md5.update(input_bytes)
return md5.digest()
def hook_sha256_digest(emu, input_bytes):
"""模拟 SHA-256"""
sha = hashlib.sha256()
sha.update(input_bytes)
return sha.digest()
处理 HMAC 签名
import hmac
import hashlib
def hook_hmac_sha256(emu, key, data):
"""模拟 HMAC-SHA256"""
signature = hmac.new(key, data, hashlib.sha256).digest()
return signature
完整案例:模拟调用某 APP 的签名函数
下面通过一个完整的案例,展示使用 AndroidNativeEmu 模拟调用签名函数的全过程。
案例背景
假设我们分析一个 APP 的签名算法,其 SO 文件 libsign.so 导出了一个 sign 函数,签名的输入包含:
- 用户 ID(字符串)
- 请求参数(字节数组)
- 时间戳(长整型)
完整代码
import logging
from androidemu.emulator import Emulator
from androidemu.java.java_class import JavaClass
from androidemu.java.java_string import JavaString
from androidemu.java.java_array import JavaByteArray
# 配置日志
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class SignEmulator:
def __init__(self, so_path):
# 1. 创建模拟器
self.emulator = Emulator()
# 2. 配置 Java 方法模拟
self._setup_java_methods()
# 3. 加载 SO 文件
self.lib = self.emulator.load_library(so_path)
logger.info("SO 文件加载完成")
def _setup_java_methods(self):
"""设置需要模拟的 Java 方法"""
emu = self.emulator
# 模拟 System.currentTimeMillis()
emu.java_method_manager.add_method(
"java/lang/System",
"currentTimeMillis",
"()J",
lambda e: 1700000000000
)
# 模拟 String.getBytes()
emu.java_method_manager.add_method(
"java/lang/String",
"getBytes",
"()[B",
lambda e, s: e.create_java_byte_array(s.encode('utf-8'))
)
# 模拟 String.length()
emu.java_method_manager.add_method(
"java/lang/String",
"length",
"()I",
lambda e, s: len(s)
)
# 模拟 Base64 编码
emu.java_method_manager.add_method(
"android/util/Base64",
"encodeToString",
"([BI)Ljava/lang/String;",
lambda e, data, flags: e.create_java_string(
__import__('base64').b64encode(data).decode()
)
)
def sign(self, user_id, params, timestamp=None):
"""调用签名函数"""
# 准备参数
j_user_id = self.emulator.create_java_string(user_id)
j_params = self.emulator.create_java_byte_array(params)
j_timestamp = timestamp if timestamp else 1700000000000
try:
# 调用导出函数
result = self.emulator.call_symbol(
"Java_com_example_app_SignUtils_sign",
self.emulator.jnienv.address, # JNIEnv*
0, # jobject (this)
j_user_id, # userId
j_params, # params
j_timestamp # timestamp
)
# 读取返回的字符串
if result:
sign_str = self.emulator.read_java_string(result)
logger.info(f"签名结果: {sign_str}")
return sign_str
else:
logger.error("签名函数返回空")
return None
except Exception as e:
logger.error(f"签名失败: {e}")
return None
# 使用示例
if __name__ == "__main__":
emu = SignEmulator("lib/armeabi-v7a/libsign.so")
result = emu.sign(
user_id="user_12345",
params=b"param1=value1¶m2=value2"
)
print(f"最终签名: {result}")
处理常见的错误
在模拟执行过程中,可能会遇到以下常见错误:
1. JNI 函数未实现
[ERROR] JNI function not implemented: FindClass(0x80000001, "com/example/Utils")
解决方法:使用 add_method 或 hook 机制添加对应的 Java 方法模拟。
2. 内存访问越界
[ERROR] Invalid memory access at 0x12345678
解决方法:检查内存映射是否完整,可能需要映射额外的内存区域。
3. 执行超时
[ERROR] Emulation timeout
解决方法:可能是代码陷入了死循环,需要检查循环条件和退出逻辑。
4. 未解析的导入符号
[ERROR] Unresolved symbol: openssl_md5
解决方法:加载缺失的依赖 SO 文件,或通过 hook 替换缺失的函数实现。
调试技巧
开启详细日志
import logging
logging.basicConfig(
level=logging.DEBUG,
format='[%(levelname)s] %(message)s'
)
Hook 特定函数
from androidemu.hook import Hook
hook = Hook(self.emulator)
# hook 特定的导入函数
@hook.hook_function("libnative.so", "openssl_encrypt")
def openssl_encrypt_hook(emu, *args):
print(f"openssl_encrypt 被调用")
print(f" 参数1: {args[0]}")
print(f" 参数2: {args[1]}")
# 返回自定义结果
return emulator.create_java_string("hooked_result")
读取寄存器和内存
# 在关键位置添加 hook
@hook.hook_code(0x1000, 0x2000)
def debug_hook(emu, address, size, user_data):
r0 = emu.reg_read(0) # ARM R0
print(f"PC={address:#x}, R0={r0:#x}")
AndroidNativeEmu 适合快速验证 SO 文件中的算法逻辑,特别是当只需要调用一两个简单的 Native 函数时。但对于复杂的 SO 文件(依赖大量系统库、复杂的 JNI 交互),Unidbg 可能是更好的选择,因为它的 JNI 模拟更加完善。